﻿using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Media;
using Gmantis.Zip;

namespace Gmantis.Imaging
{
    public static class PngCodec
    {
        // Fields
        private static Color _emptyColor = Color.FromArgb(0, 0, 0, 0);
        internal const string bKGD = "bKGD";
        internal const string IDAT = "IDAT";
        internal const string IEND = "IEND";
        internal const string IHDR = "IHDR";
        internal const string PLTE = "PLTE";
        internal static byte[] Signature = new byte[] { 0x89, 80, 0x4e, 0x47, 13, 10, 0x1a, 10 };
        internal const string tRNS = "tRNS";

        // Methods
        private static Color ApplyGamma(Color color, double gamma)
        {
            return Color.FromArgb(color.A, ApplyGamma(color.R, gamma, 8), ApplyGamma(color.G, gamma, 8), ApplyGamma(color.B, gamma, 8));
        }

        private static byte ApplyGamma(int sample, double gamma, int sampleDepth)
        {
            double x = ((double)sample) / ((double)((((int)1) << sampleDepth) - 1));
            return (byte)Math.Floor((double)((Math.Pow(x, 1.0 / (gamma * 2.2)) * 255.0) + 0.5));
        }

        private static void BuildImage(Bitmap bmp, PngData png)
        {
            bmp.SetSize(png.Width, png.Height);
            switch (((ColorTypeEnum)((byte)png.ColorType)))
            {
                case ColorTypeEnum.GrayScale:
                    BuildImageGrayScale(bmp, png, false);
                    return;

                case ((ColorTypeEnum)1):
                case ((ColorTypeEnum)5):
                    break;

                case ColorTypeEnum.TrueColor:
                    BuildImageTrueColor(bmp, png, false);
                    return;

                case ColorTypeEnum.Indexed:
                    BuildImageIndexed(bmp, png);
                    return;

                case ColorTypeEnum.GrayScaleAlpha:
                    BuildImageGrayScale(bmp, png, true);
                    return;

                case ColorTypeEnum.TrueColorAlpha:
                    BuildImageTrueColor(bmp, png, true);
                    break;

                default:
                    return;
            }
        }

        private static void BuildImageGrayScale(Bitmap image, PngData png, bool alpha)
        {
            int bitDepth = png.BitDepth;
            if (alpha)
            {
                if ((bitDepth != 8) && (bitDepth != 0x10))
                {
                    throw new BadImageFormatException("Grayscale alpha image should have 8 or 16 bpp.");
                }
            }
            else if ((((bitDepth != 1) && (bitDepth != 2)) && ((bitDepth != 4) && (bitDepth != 8))) && (bitDepth != 0x10))
            {
                throw new BadImageFormatException("Grayscale image should have 1, 2, 4, 8, or 16 bpp.");
            }
            int num2 = (((int)1) << bitDepth) - 1;
            byte[] imageData = png.GetImageData();
            int index = 0;
            for (int i = 0; i < image.Height; i++)
            {
                index++;
                for (int j = 0; j < image.Width; j++)
                {
                    if (bitDepth == 0x10)
                    {
                        ushort num6 = (ushort)((0xff * imageData[index]) + imageData[index + 1]);
                        index += 2;
                        byte a = 0xff;
                        if (alpha)
                        {
                            a = (byte)(((0xff * imageData[index]) + imageData[index + 1]) / 0x100);
                            index += 2;
                        }
                        else if ((png.TrnsColor != null) && (png.TrnsColor[0] == num6))
                        {
                            a = 0;
                            if (png.BkgdColor != null)
                            {
                                num6 = png.BkgdColor[0];
                            }
                        }
                        byte r = (byte)(num6 >> 8);
                        Color color = Color.FromArgb(a, r, r, r);
                        image.SetPixel(j, i, color);
                    }
                    else
                    {
                        byte num9 = imageData[index++];
                        for (int k = 8 - bitDepth; (k >= 0) && (j < image.Width); k -= bitDepth)
                        {
                            byte num11 = (byte)((num9 >> k) & num2);
                            byte num12 = 0xff;
                            if (alpha)
                            {
                                num12 = imageData[index++];
                            }
                            else if ((png.TrnsColor != null) && (png.TrnsColor[0] == num11))
                            {
                                num12 = 0;
                                if (png.BkgdColor != null)
                                {
                                    num11 = (byte)png.BkgdColor[0];
                                }
                            }
                            byte num13 = (byte)((0xff * num11) / num2);
                            Color color2 = Color.FromArgb(num12, num13, num13, num13);
                            image.SetPixel(j, i, color2);
                            if (k > 0)
                            {
                                j++;
                            }
                        }
                    }
                }
            }
        }

        private static void BuildImageIndexed(Bitmap image, PngData png)
        {
            Color[] palette = png.Palette;
            if (palette == null)
            {
                throw new BadImageFormatException("Invalid format (missing palette).");
            }
            int bitDepth = png.BitDepth;
            if (((bitDepth != 1) && (bitDepth != 2)) && ((bitDepth != 4) && (bitDepth != 8)))
            {
                throw new BadImageFormatException("Indexed image should have 1, 2, 4, or 8 bpp.");
            }
            int num2 = (((int)1) << bitDepth) - 1;
            byte[] imageData = png.GetImageData();
            int num3 = 0;
            for (int i = 0; i < image.Height; i++)
            {
                num3++;
                for (int j = 0; j < image.Width; j++)
                {
                    byte num6 = imageData[num3++];
                    for (int k = 8 - bitDepth; (k >= 0) && (j < image.Width); k -= bitDepth)
                    {
                        image.SetPixel(j, i, palette[(num6 >> k) & num2]);
                        if (k > 0)
                        {
                            j++;
                        }
                    }
                }
            }
        }

        private static void BuildImageTrueColor(Bitmap image, PngData png, bool alpha)
        {
            int bitDepth = png.BitDepth;
            if ((bitDepth != 8) && (bitDepth != 0x10))
            {
                throw new BadImageFormatException("TrueColor image should have 8 or 16 bpp.");
            }
            byte[] imageData = png.GetImageData();
            int index = 0;
            for (int i = 0; i < image.Height; i++)
            {
                index++;
                for (int j = 0; j < image.Width; j++)
                {
                    Color color = new Color();
                    if (bitDepth == 8)
                    {
                        color.R = imageData[index++];
                        color.G = imageData[index++];
                        color.B = imageData[index++];
                        if (alpha)
                        {
                            color.A = imageData[index++];
                        }
                        else if (((png.TrnsColor != null) && (png.TrnsColor[0] == color.R)) && ((png.TrnsColor[1] == color.G) && (png.TrnsColor[2] == color.B)))
                        {
                            color.A = 0;
                            if (png.BkgdColor != null)
                            {
                                color.R = (byte)png.BkgdColor[0];
                                color.G = (byte)png.BkgdColor[1];
                                color.B = (byte)png.BkgdColor[2];
                            }
                        }
                        else
                        {
                            color.A = 0xff;
                        }
                    }
                    else
                    {
                        ushort num5 = (ushort)((0x100 * imageData[index]) + imageData[index + 1]);
                        index += 2;
                        ushort num6 = (ushort)((0x100 * imageData[index]) + imageData[index + 1]);
                        index += 2;
                        ushort num7 = (ushort)((0x100 * imageData[index]) + imageData[index + 1]);
                        index += 2;
                        if (alpha)
                        {
                            color.A = (byte)(((0x100 * imageData[index]) + imageData[index + 1]) / 0x100);
                            index += 2;
                        }
                        else if (((png.TrnsColor != null) && (png.TrnsColor[0] == num5)) && ((png.TrnsColor[1] == num6) && (png.TrnsColor[2] == num7)))
                        {
                            color.A = 0;
                            if (png.BkgdColor != null)
                            {
                                num5 = png.BkgdColor[0];
                                num6 = png.BkgdColor[1];
                                num7 = png.BkgdColor[2];
                            }
                        }
                        else
                        {
                            color.A = 0xff;
                        }
                        color.R = (byte)(num5 >> 8);
                        color.G = (byte)(num6 >> 8);
                        color.B = (byte)(num7 >> 8);
                    }
                    image.SetPixel(j, i, color);
                }
            }
        }

        public static Bitmap Decode(Stream stream)
        {
            Bitmap bmp = new Bitmap();
            Decode(bmp, stream);
            return bmp;
        }

        internal static void Decode(Bitmap bmp, Stream stream)
        {
            try
            {
                bmp.BeginUpdate();
                DecodeInternal(bmp, stream);
            }
            finally
            {
                bmp.EndUpdate();
            }
        }

        private static void DecodeInternal(Bitmap bmp, Stream stream)
        {
            PngData png = new PngData();
            EndianReader reader = new EndianReader(stream);
            byte[] buffer = new byte[Signature.Length];
            stream.Read(buffer, 0, buffer.Length);
            if (!Bitmap.Match(buffer, Signature))
            {
                throw new BadImageFormatException("PNG signature not found.");
            }
            bool flag = false;
            while (!flag)
            {
                int count = reader.ReadInt32BigEndian();
                byte[] typeBytes = reader.ReadBytes(4);
                byte[] data = reader.ReadBytes(count);
                long checkCrc = reader.ReadUInt32BigEndian();
                VerifyCrc(typeBytes, data, checkCrc);
                string str = Encoding.UTF8.GetString(typeBytes, 0, typeBytes.Length);
                if (str != null)
                {
                    if (!(str == "IHDR"))
                    {
                        if (str == "PLTE")
                        {
                            goto Label_00EE;
                        }
                        if (str == "tRNS")
                        {
                            goto Label_00F8;
                        }
                        if (str == "bKGD")
                        {
                            goto Label_0102;
                        }
                        if (str == "IDAT")
                        {
                            goto Label_010C;
                        }
                        if (str == "IEND")
                        {
                            goto Label_0116;
                        }
                    }
                    else
                    {
                        png.ParseHeader(data);
                    }
                }
                continue;
            Label_00EE:
                png.ParsePalette(data);
                continue;
            Label_00F8:
                png.ParseTransparency(data);
                continue;
            Label_0102:
                png.ParseBackground(data);
                continue;
            Label_010C:
                png.ParseImageData(data);
                continue;
            Label_0116:
                flag = true;
            }
            BuildImage(bmp, png);
        }

        public static Stream Encode(Bitmap bmp)
        {
            return Encode(bmp, true);
        }

        public static Stream Encode(Bitmap bmp, bool compress)
        {
            return Encode(bmp, compress, true);
        }

        internal static Stream Encode(Bitmap bmp, bool compress, bool useChecksum)
        {
            byte[] imageData = bmp.GetImageData();
            int width = bmp.Width;
            int height = bmp.Height;
            ColorTypeEnum enum2 = compress ? GetOptimalColorType(imageData, width, height) : ColorTypeEnum.TrueColorAlpha;
            byte[] zlibHeader = null;
            if (compress || useChecksum)
            {
                MemoryStream baseStream = new MemoryStream(imageData.Length / 2);
                ZStreamWriter writer = new ZStreamWriter(baseStream, compress ? CompressionLevelEnum.DefaultCompression : CompressionLevelEnum.NoCompression);
                switch (enum2)
                {
                    case ColorTypeEnum.GrayScale:
                    case ColorTypeEnum.GrayScaleAlpha:
                        {
                            int count = (enum2 == ColorTypeEnum.GrayScaleAlpha) ? 2 : 1;
                            int num8 = 0;
                            int num9 = 0;
                            while (num8 < height)
                            {
                                num9 += bmp.RowExtra - 1;
                                writer.WriteByte(imageData[num9++]);
                                for (int i = 0; i < width; i++)
                                {
                                    writer.Write(imageData, num9 + 2, count);
                                    num9 += 4;
                                }
                                num8++;
                            }
                            break;
                        }
                    case ColorTypeEnum.TrueColor:
                    case ColorTypeEnum.TrueColorAlpha:
                        {
                            int num3 = (enum2 == ColorTypeEnum.TrueColorAlpha) ? 4 : 3;
                            int num4 = 0;
                            int offset = 0;
                            while (num4 < height)
                            {
                                offset += bmp.RowExtra - 1;
                                writer.WriteByte(imageData[offset++]);
                                for (int j = 0; j < width; j++)
                                {
                                    writer.Write(imageData, offset, num3);
                                    offset += 4;
                                }
                                num4++;
                            }
                            break;
                        }
                }
                writer.Close();
                byte[] src = baseStream.ToArray();
                zlibHeader = new byte[] { src[0], src[1] };
                imageData = new byte[src.Length - 2];
                Buffer.BlockCopy(src, 2, imageData, 0, imageData.Length);
            }
            return new PngStream(imageData, zlibHeader, width, height, (byte)enum2, useChecksum);
        }

        private static ColorTypeEnum GetOptimalColorType(byte[] data, int width, int height)
        {
            bool flag = false;
            bool flag2 = false;
            int index = 0;
            for (int i = 0; i < height; i++)
            {
                index++;
                for (int j = 0; j < width; j++)
                {
                    if (!flag && ((data[index] != data[index + 1]) || (data[index] != data[index + 2])))
                    {
                        flag = true;
                    }
                    if (!flag2 && (data[index + 3] != 0xff))
                    {
                        flag2 = true;
                    }
                    if (flag && flag2)
                    {
                        return ColorTypeEnum.TrueColorAlpha;
                    }
                    index += 4;
                }
            }
            if (flag)
            {
                if (!flag2)
                {
                    return ColorTypeEnum.TrueColor;
                }
                return ColorTypeEnum.TrueColorAlpha;
            }
            if (!flag2)
            {
                return ColorTypeEnum.GrayScale;
            }
            return ColorTypeEnum.GrayScaleAlpha;
        }

        private static bool VerifyCrc(byte[] typeBytes, byte[] data, long checkCrc)
        {
            Crc32 crc = new Crc32();
            crc.UpdateCrc(typeBytes);
            crc.UpdateCrc(data);
            return (crc.Value == checkCrc);
        }

        // Nested Types
        internal enum ColorTypeEnum : byte
        {
            GrayScale = 0,
            GrayScaleAlpha = 4,
            Indexed = 3,
            TrueColor = 2,
            TrueColorAlpha = 6
        }

        private class PngData
        {
            // Fields
            public int BitDepth;
            public ushort[] BkgdColor;
            public int ColorType;
            public int Compression;
            public int Filter;
            public int Height;
            public Stream ImageStream = new MemoryStream();
            public int InterlaceMode;
            public Color[] Palette;
            private static readonly InterlacePass[] passes = new InterlacePass[] { new InterlacePass(0, 0, 8, 8), new InterlacePass(0, 4, 8, 8), new InterlacePass(4, 0, 8, 4), new InterlacePass(0, 2, 4, 4), new InterlacePass(2, 0, 4, 2), new InterlacePass(0, 1, 2, 2), new InterlacePass(1, 0, 2, 1) };
            public ushort[] TrnsColor;
            public int Width;

            // Methods
            private void Deinterlace(InterlacePass pass, byte[] data, int offset, int width, int height, int rowLength, byte[] newData)
            {
                Func<int, int, int, int> func2 = null;
                for (int i = 0; i < height; i++)
                {
                    for (int j = 0; j < width; j++)
                    {
                        if (func2 == null)
                        {
                            func2 = (r, c, l) => (((r * l) * 8) + ((c * this.BitDepth) * this.Channels)) + 8;
                        }
                        Func<int, int, int, int> func = func2;
                        int num3 = func(i, j, rowLength) + (offset * 8);
                        int num4 = num3 % 8;
                        int num5 = (i * pass.RowIncrement) + pass.RowStart;
                        int num6 = (j * pass.ColIncrement) + pass.ColStart;
                        int num7 = func(num5, num6, this.RowLength);
                        int num8 = num7 % 8;
                        byte num9 = (this.BitDepth < 8) ? ((byte)((((1 << (this.BitDepth & 0x1f)) - 1) & 0xff) << (8 - this.BitDepth))) : ((byte)0xff);
                        for (int k = 0; k < ((int)Math.Ceiling((double)(((double)(this.BitDepth * this.Channels)) / 8.0))); k++)
                        {
                            newData[(num7 / 8) + k] = (byte)(newData[(num7 / 8) + k] | ((byte)(((data[(num3 / 8) + k] & (num9 >> (num4 & 0x1f))) << (num4 & 0x1f)) >> num8)));
                        }
                    }
                }
            }

            public byte[] GetImageData()
            {
                this.ImageStream.Position = 0L;
                ZStreamReader reader = new ZStreamReader(this.ImageStream);
                MemoryStream stream = new MemoryStream(((int)this.ImageStream.Length) * 2);
                byte[] buffer = new byte[0x8000];
                while (true)
                {
                    int count = reader.Read(buffer, 0, buffer.Length);
                    if (count == 0)
                    {
                        break;
                    }
                    stream.Write(buffer, 0, count);
                }
                stream.Flush();
                byte[] data = stream.ToArray();
                return this.Untangle(data);
            }

            private static short GetUnsignedByte(byte[] data, int idx)
            {
                return (short)(data[idx] & 0xff);
            }

            private static int GetUnsignedInt(byte[] data, int idx)
            {
                int num = 0;
                for (int i = 0; i < 4; i++)
                {
                    num += (data[idx + i] & 0xff) << ((3 - i) * 8);
                }
                return num;
            }

            public void ParseBackground(byte[] data)
            {
                switch (((PngCodec.ColorTypeEnum)((byte)this.ColorType)))
                {
                    case PngCodec.ColorTypeEnum.GrayScale:
                        this.BkgdColor = new ushort[] { (ushort)((data[0] * 0xff) + data[1]) };
                        return;

                    case ((PngCodec.ColorTypeEnum)1):
                        break;

                    case PngCodec.ColorTypeEnum.TrueColor:
                        this.BkgdColor = new ushort[] { (ushort)((data[0] << 8) | data[1]), (ushort)((data[2] << 8) | data[3]), (ushort)((data[4] << 8) | data[5]) };
                        break;

                    case PngCodec.ColorTypeEnum.Indexed:
                        this.BkgdColor = new ushort[] { data[0] };
                        return;

                    default:
                        return;
                }
            }

            public void ParseHeader(byte[] data)
            {
                this.Width = GetUnsignedInt(data, 0);
                this.Height = GetUnsignedInt(data, 4);
                this.BitDepth = GetUnsignedByte(data, 8);
                this.ColorType = GetUnsignedByte(data, 9);
                this.Compression = GetUnsignedByte(data, 10);
                this.Filter = GetUnsignedByte(data, 11);
                this.InterlaceMode = GetUnsignedByte(data, 12);
            }

            public void ParseImageData(byte[] data)
            {
                this.ImageStream.Write(data, 0, data.Length);
            }

            public void ParsePalette(byte[] data)
            {
                if ((data.Length % 3) != 0)
                {
                    throw new BadImageFormatException("Invalid palette (must contain multiple of three entries)");
                }
                this.Palette = new Color[data.Length / 3];
                for (int i = 0; i < this.Palette.Length; i++)
                {
                    int index = 3 * i;
                    this.Palette[i] = Color.FromArgb(0xff, data[index], data[index + 1], data[index + 2]);
                }
            }

            public void ParseTransparency(byte[] data)
            {
                switch (((PngCodec.ColorTypeEnum)((byte)this.ColorType)))
                {
                    case PngCodec.ColorTypeEnum.GrayScale:
                        this.TrnsColor = new ushort[] { (ushort)((data[0] * 0xff) + data[1]) };
                        return;

                    case ((PngCodec.ColorTypeEnum)1):
                        break;

                    case PngCodec.ColorTypeEnum.TrueColor:
                        this.TrnsColor = new ushort[] { (ushort)((data[0] << 8) | data[1]), (ushort)((data[2] << 8) | data[3]), (ushort)((data[4] << 8) | data[5]) };
                        break;

                    case PngCodec.ColorTypeEnum.Indexed:
                        if (this.Palette != null)
                        {
                            for (int i = 0; i < data.Length; i++)
                            {
                                if (i >= this.Palette.Length)
                                {
                                    return;
                                }
                                this.Palette[i].A = data[i];
                            }
                        }
                        break;

                    default:
                        return;
                }
            }

            private void Unfilter(byte[] data, int offset, int rowLength, int height)
            {
                int bytesPerPixel = this.BytesPerPixel;
                int num2 = 0;
                int index = offset;
                while (num2 < height)
                {
                    byte num4 = data[index];
                    if (num4 == 0)
                    {
                        index += rowLength;
                    }
                    else
                    {
                        index++;
                        int num5 = 1;
                        while (num5 < rowLength)
                        {
                            byte num1 = data[index];
                            byte num6 = (num5 < (bytesPerPixel + 1)) ? ((byte)0) : data[index - bytesPerPixel];
                            byte num7 = (num2 == 0) ? ((byte)0) : data[index - rowLength];
                            byte num8 = ((num2 == 0) || (num5 < (bytesPerPixel + 1))) ? ((byte)0) : data[(index - rowLength) - bytesPerPixel];
                            switch (num4)
                            {
                                case 1:
                                    data[index] = (byte)(data[index] + num6);
                                    break;

                                case 2:
                                    data[index] = (byte)(data[index] + num7);
                                    break;

                                case 3:
                                    data[index] = (byte)(data[index] + ((byte)Math.Floor((double)((num6 + num7) * 0.5))));
                                    break;

                                case 4:
                                    {
                                        int num9 = (num6 + num7) - num8;
                                        int num10 = Math.Abs((int)(num9 - num6));
                                        int num11 = Math.Abs((int)(num9 - num7));
                                        int num12 = Math.Abs((int)(num9 - num8));
                                        data[index] = (byte)(data[index] + (((num10 <= num11) && (num10 <= num12)) ? num6 : ((num11 <= num12) ? num7 : num8)));
                                        break;
                                    }
                            }
                            num5++;
                            index++;
                        }
                    }
                    num2++;
                }
            }

            private byte[] Untangle(byte[] data)
            {
                if (this.InterlaceMode == 0)
                {
                    this.Unfilter(data, 0, this.RowLength, this.Height);
                    return data;
                }
                byte[] newData = new byte[this.RowLength * this.Height];
                int offset = 0;
                foreach (InterlacePass pass in passes)
                {
                    if ((this.Width > pass.ColStart) && (this.Height > pass.RowStart))
                    {
                        int width = (int)Math.Ceiling((double)(((this.Width - pass.ColStart) * 1.0) / ((double)pass.ColIncrement)));
                        int rowLength = 1 + ((int)Math.Ceiling((double)(((double)((this.BitDepth * this.Channels) * width)) / 8.0)));
                        int height = (int)Math.Ceiling((double)(((this.Height - pass.RowStart) * 1.0) / ((double)pass.RowIncrement)));
                        this.Unfilter(data, offset, rowLength, height);
                        this.Deinterlace(pass, data, offset, width, height, rowLength, newData);
                        offset += rowLength * height;
                    }
                }
                return newData;
            }

            // Properties
            public int BytesPerPixel
            {
                get
                {
                    return Math.Max((this.BitDepth * this.Channels) / 8, 1);
                }
            }

            public int Channels
            {
                get
                {
                    switch (((PngCodec.ColorTypeEnum)((byte)this.ColorType)))
                    {
                        case PngCodec.ColorTypeEnum.GrayScale:
                        case PngCodec.ColorTypeEnum.Indexed:
                            return 1;

                        case PngCodec.ColorTypeEnum.TrueColor:
                            return 3;

                        case PngCodec.ColorTypeEnum.GrayScaleAlpha:
                            return 2;

                        case PngCodec.ColorTypeEnum.TrueColorAlpha:
                            return 4;
                    }
                    return 1;
                }
            }

            public int RowLength
            {
                get
                {
                    return (1 + ((int)Math.Ceiling((double)(((double)((this.BitDepth * this.Channels) * this.Width)) / 8.0))));
                }
            }

            // Nested Types
            [StructLayout(LayoutKind.Sequential)]
            private struct InterlacePass
            {
                public int RowStart;
                public int ColStart;
                public int RowIncrement;
                public int ColIncrement;
                public InterlacePass(int rowStart, int colStart, int rowIncrement, int colIncrement)
                {
                    this.RowStart = rowStart;
                    this.ColStart = colStart;
                    this.RowIncrement = rowIncrement;
                    this.ColIncrement = colIncrement;
                }
            }
        }
    }
}
